#include "stdafx.h"
#include "ClientSystem.h"
#include "ServerHandler.h"

#include "Core\ClientConnection.h"
#include "Core\XMLConnection.h"
#include "Core\Packages.h"
#include "Core\CoreXMLHandlerManager.h"
#include "Core\Logger.h"

#include "Poco\Thread.h"
#include "Poco\Delegate.h"
#include "Poco\File.h"
#include "Poco\DateTime.h"
#include "Poco\DateTimeFormatter.h"
#include "Poco\Timezone.h"
#include "Poco\Process.h"

namespace ExamSystem
{
    using Poco::Thread;
    using Poco::Delegate;

namespace Client
{
    // the only instance
    CClientSystem theClientSystem;

    CClientSystem::CClientSystem(void)
        : m_synLogin(false)
        , m_loginResult(CLoginResultPackage::Error)
        , m_examFolder("C:\\exam\\")
        , m_flagQuestionsReceived(false)
        , m_synQuestionCriterion(false)
        , m_synSendingAnswerDatabase(false)
        , m_synInitialization(false)
        , m_flagAnswerDatabaseSent(false)
        , m_flagInitialized(false)
    {
        m_examInformation.examStatus = ExamInformation::Preparing;
        m_examInformation.examQuestionGenerationMethod = ExamInformation::Random;
        m_examInformation.examRestTime = 0;
        m_examInformation.examTotalTime = 0;
        m_examInformation.examStartTime = 0;
        m_examInformation.examEndTime = 0;
        m_synLogin.set();
        m_synQuestionCriterion.reset();
        m_synSendingAnswerDatabase.set();
        m_synInitialization.reset();
    }

    CClientSystem::~CClientSystem(void)
    {

    }

    void CClientSystem::SetRootServerAddress(const std::string &host, Poco::UInt16 port)
    {
        m_rootServerAddress = SocketAddress(host, port);
    }

    void CClientSystem::SetRootServerAddress(const SocketAddress &address)
    {
        m_rootServerAddress = address;
    }

    bool CClientSystem::Initialize(void)
    {
        if (m_flagInitialized)
            return true;

        // initialize the system logger
        CExamLogger::CreateLogger("Client.log");
        systemLogger.information("Client start.");

        if (!ConnectServer())
        {
            systemLogger.fatal("Can't connect to server.");
            return false;
        }

        m_flagInitialized = true;

        return true;
    }

    void CClientSystem::Finalize(void)
    {
        if (!m_flagInitialized)
            return;

        DisconnectServer();
        systemLogger.information("Client stopped.");
        m_synInitialization.reset();

        m_questionLibrary.Finalize();
        m_answerDatabase.Finalize();
        
        if (!m_questionLibraryFile.empty())
        {
            Poco::File file(m_questionLibraryFile);
            try {
                file.remove();
            } catch (...) {}
        }
        if (m_flagAnswerDatabaseSent && !m_answerDatabaseFile.empty())
        {
            Poco::File file(m_answerDatabaseFile);
            try {
                file.remove();
            }catch(...){}
        }

        try {
            Poco::File file(m_examFolder);
            file.remove(true);
        }
        catch (...) {}


        m_flagInitialized = false;
    }

    void CClientSystem::StudentLogin(const std::string &name, const std::string &number)
    {
        m_synInitialization.wait();
        m_synLogin.reset();
        m_loginResult = CLoginResultPackage::Error;
        CPackagePtr login = packageManager.CreatePackage(CLoginPackage::packageType);
        static_cast<CLoginPackage *>(login.get())->m_username = name;
        static_cast<CLoginPackage *>(login.get())->m_password = number;
        if (m_serverConnection && m_serverConnection->GetXMLConnection())
            login->WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());

        m_loginStudent.studentName = name;
        m_loginStudent.studentNumber = number;
    }

    void CClientSystem::InvigilatorLogin(const std::string &user, const std::string &password)
    {
        m_synInitialization.wait();
        m_synLogin.reset();
        m_loginResult = CLoginResultPackage::Error;
        CPackagePtr login = packageManager.CreatePackage(CLoginPackage::packageType);
        static_cast<CLoginPackage *>(login.get())->m_username = user;
        static_cast<CLoginPackage *>(login.get())->m_password = password;
        if (m_serverConnection && m_serverConnection->GetXMLConnection())
            login->WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());
    }

    bool CClientSystem::IsExamStarted(void) const
    {
        return m_examInformation.examStatus == ExamInformation::Processing;
    }

    bool CClientSystem::IsExamFinished(void) const
    {
        return m_examInformation.examStatus == ExamInformation::Finished;
    }

    void CClientSystem::RespondToOrder(COrderPackage &order)
    {
        int i = 0;
        switch (order.m_order)
        {
        case COrderPackage::StartExam:
            m_examInformation.examStatus = ExamInformation::Processing;
            m_examInformation.examStartTime = (unsigned int)time(NULL);
            m_examInformation.examEndTime = m_examInformation.examStartTime + m_examInformation.examTotalTime;
            eventExamStarted(this, i);
            break;

        case COrderPackage::StopExam:
            m_examInformation.examStatus = ExamInformation::Finished;
            eventExamStopped(this, i);
            break;
        }
    }

    void CClientSystem::RespondToMessage(CMessagePackage &message)
    {
        if (message.m_messageType == CMessagePackage::Alert)
            eventExamAlert(this, message.m_messageBody);
        else
            eventExamMessage(this, message.m_messageBody);
    }

    void CClientSystem::SetExamInformation(const ExamInformation &information)
    {
        m_examInformation.examIdentity = information.examIdentity;
        m_examInformation.examDateTime = information.examDateTime;
        m_examInformation.examStatus = information.examStatus;
        m_examInformation.examQuestionGenerationMethod = information.examQuestionGenerationMethod;
        for (int i = 0; i < QuestionTypeCount; i++)
        {
            m_examInformation.examQuestions[i] = information.examQuestions[i];
            m_examInformation.examQuestionCoefs[i] = information.examQuestionCoefs[i];
        }
        m_examInformation.examTotalTime = information.examTotalTime;
        m_examInformation.examRestTime = information.examRestTime;

        m_examInformation.examStartTime = 0;
        m_examInformation.examEndTime = 0;

        if (m_examInformation.examStatus == ExamInformation::Processing)
        {
            unsigned int currentTime = (unsigned int)time(NULL);
            m_examInformation.examStartTime = currentTime - (m_examInformation.examTotalTime - m_examInformation.examRestTime);
            m_examInformation.examEndTime = currentTime + m_examInformation.examRestTime;
        }
    }

    void CClientSystem::OnServerConnectionClosed(const void* pSender, int &id)
    {
        m_serverConnectionHandler = NULL;

        if (m_flagValuntaryClosure)
            return;

        m_zombieServerConnection = m_serverConnection;

        if (m_flagConnectionRedirected)
            systemLogger.error("Disconnected from server, trying to reconnect root server...");
        else
            systemLogger.error("Disconnected from root server, trying to reconnect...");

        static const int maxRetryTime = 10;
        static const int sleepTime = 5000;
        int nRetryTime = 0;
        while (true)
        {
            if (ConnectServer())
                break;

            nRetryTime++;
            if (nRetryTime < maxRetryTime)
            {
                systemLogger.error(Poco::format("The (%d/%d)-th try failed. Wait 5 seconds to retry...", nRetryTime, maxRetryTime));
                Thread::current()->sleep(sleepTime);
            }
            else
            {   
                systemLogger.error(Poco::format("The (%d/%d)-th try failed.", nRetryTime, maxRetryTime));
                break;
            }            
        }

        if (nRetryTime == maxRetryTime)
        {
            // failed
            systemLogger.fatal("Failed to reconnect.");
            eventServerConnectionClosed(this, this);
        }
    }

    void CClientSystem::OnServerConnectionDisconnected(const void* pSender, int &id)
    {     
        m_serverConnectionHandler = NULL;
    }

    void CClientSystem::OnServerConnectionReconnected(const void *pSender, int &id)
    {
        CreateHandler();

        m_flagConnectionRedirected = true;
    }

    void CClientSystem::OnServerConnectionRedirected(const void *pSender, SocketAddress &targetAddress)
    {
        if (targetAddress.host().isLoopback())
            targetAddress = SocketAddress(m_rootServerAddress.host(), targetAddress.port());

        m_serverConnection->SetServerAddress(targetAddress);
        m_serverConnection->ReconnectServer();
    }

    void CClientSystem::OnServerConnectionVersionCheckFailed(const void *pSender, void *const& pConnection)
    {
        m_serverConnection->DisconnectServer(false);
    }

    void CClientSystem::OnServerConnectionReady(const void *pSender, void *const& pConnection)
    {
        if (m_loginResult == CLoginResultPackage::Succeeded)
        {
            SendConnectionInfo();
            InitializeExamFolder();
            if (!m_flagQuestionsReceived)
                Client::theClientSystem.RequireQuestions();
        }

        m_synInitialization.set();
    }

    void CClientSystem::SendConnectionInfo(void)
    {
        // send connection info
        CPackagePtr pkg = packageManager.CreatePackage(CConnectionInfoPackage::packageType);
        CConnectionInfoPackage &info = *static_cast<CConnectionInfoPackage *>(pkg.get());

        info.m_sourceIdentity = CConnectionInfoPackage::Client;
        info.m_connectionStatus = CConnectionInfoPackage::Connected;
        info.m_studentName = m_loginStudent.studentName;
        info.m_studentNumber = m_loginStudent.studentNumber;
        info.m_routePath.push_back(systemID);

        if (m_serverConnection && m_serverConnection->GetXMLConnection())
            info.WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());

    }

    bool CClientSystem::ConnectServer(void)
    {
        // for sub-server, we should create connection to root server;
        m_serverConnection = new CExamClientConnection;
        m_serverConnection->SetServerAddress(m_rootServerAddress);

        if (!m_serverConnection->ConnectServer())
            return false;

        m_serverConnection->eventConnectionClosed += 
            Delegate<CClientSystem, int>(this, &CClientSystem::OnServerConnectionClosed);
        m_serverConnection->eventConnectionDisconnected +=
            Delegate<CClientSystem, int>(this, &CClientSystem::OnServerConnectionDisconnected);
        m_serverConnection->eventConnectionReconnected +=
            Delegate<CClientSystem, int>(this, &CClientSystem::OnServerConnectionReconnected);

        CreateHandler();

        return true;
    }

    bool CClientSystem::CreateHandler(void)
    {
        m_serverConnectionHandler = new CServerHandler(this);
        m_serverConnectionHandler->eventConnectionRedirected +=
            Delegate<CClientSystem, SocketAddress>(this, &CClientSystem::OnServerConnectionRedirected);
        m_serverConnectionHandler->eventVersionCheckFailed +=
            Delegate<CClientSystem, void *const>(this, &CClientSystem::OnServerConnectionVersionCheckFailed);
        m_serverConnectionHandler->eventLoginResult += 
            Delegate<CClientSystem, CLoginResultPackage>(this, &CClientSystem::OnLoginResult);
        m_serverConnectionHandler->eventReady +=
            Delegate<CClientSystem, void *const>(this, &CClientSystem::OnServerConnectionReady);


        // send server identity
        SharedPtr<CPackage> pkg = packageManager.CreatePackage(CIdentityPackage::packageType);
        static_cast<CIdentityPackage *>(pkg.get())->m_identity = CIdentityPackage::Client;

        if (m_serverConnection && m_serverConnection->GetXMLConnection())
            pkg->WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());

        m_serverConnectionHandler->StartHandler();

        return true;
    }

    void CClientSystem::DisconnectServer(void)
    {
        m_flagValuntaryClosure = true;
        if (m_serverConnection)
            m_serverConnection->DisconnectServer();
    }

    CLoginResultPackage::LoginResult CClientSystem::WaitForLoginResult(void)
    {
        m_synLogin.wait();
        return m_loginResult;
    }

    CLoginResultPackage::LoginResult CClientSystem::GetLoginResult(void)
    {
        return m_loginResult;
    }

    void CClientSystem::OnLoginResult(const void *pSender, CLoginResultPackage &result)
    {
        m_loginResult = result.m_result;
        if (m_loginResult == CLoginResultPackage::Succeeded)
        {
            SendConnectionInfo();
            InitializeExamFolder();
            if (!m_flagQuestionsReceived)
                Client::theClientSystem.RequireQuestions();
        }
        m_synLogin.set();
    }

    void CClientSystem::SetExamFolder(const std::string &folder)
    {
        m_examFolder = folder;
        if (m_examFolder[m_examFolder.size() - 1] != '/' &&
            m_examFolder[m_examFolder.size() - 1] != '\\')
            m_examFolder += '\\';

        CFilePackage::ms_receivingPath = m_examFolder + "recv\\";
    }
    
    const std::string &CClientSystem::GetExamFolder(void) const
    {
        return m_examFolder;
    }

    bool CClientSystem::InitializeExamFolder(void)
    {
        Poco::File file(m_examFolder);
        file.createDirectories();

        m_questionLibraryFile = m_examFolder + "QuestionsLocal.qdb";
        Poco::DateTime dateTime;
        dateTime.makeLocal(Poco::Timezone::utcOffset());
        std::string str = Poco::DateTimeFormatter::format(dateTime, "%Y-%m-%d");

        m_answerDatabaseFile = m_examFolder + str + "_" + m_loginStudent.studentNumber + ".adb";

        Poco::File questions(m_questionLibraryFile);
        Poco::File answers(m_answerDatabaseFile);

        // if question library doesn't exist, we should create one
        m_flagQuestionsReceived = false;
        ExamInformation emptyInformation;
        if (!questions.exists() || !m_questionLibrary.Initialize(m_questionLibraryFile, false))
        {
            m_questionLibrary.Finalize();
            try{
                answers.remove();
            }catch(...){};
            try{
                questions.remove();
            }catch(...){};
            return m_questionLibrary.Initialize(m_questionLibraryFile, true)
                && m_answerDatabase.CreateDatabase(m_answerDatabaseFile, emptyInformation);
        }

        // otherwise, we should check whether the answer database is valid
        if (!answers.exists() || !m_answerDatabase.Initialize(m_answerDatabaseFile))
        {
            // there is no answer database, this may because the client crashed when
            // receiving questions. we should delete old question file and ask for 
            // question again.
            m_questionLibrary.Finalize();
            m_answerDatabase.Finalize();
            try{
                answers.remove();
            }catch(...){};
            try{
                questions.remove();
            }catch(...){};
            return m_questionLibrary.Initialize(m_questionLibraryFile, true)
                && m_answerDatabase.CreateDatabase(m_answerDatabaseFile, emptyInformation);
        }


        // check exam identity
        ExamInformation info;
        m_answerDatabase.LoadExamInformation(info);
        if (info.examIdentity != m_examInformation.examIdentity)
        {
            m_questionLibrary.Finalize();
            m_answerDatabase.Finalize();
            try{
                answers.remove();
            }catch(...){};
            try{
                questions.remove();
            }catch(...){};
            return m_questionLibrary.Initialize(m_questionLibraryFile, true)
                && m_answerDatabase.CreateDatabase(m_answerDatabaseFile, emptyInformation);
        }

        // check student
        std::vector<StudentInfo> students;
        std::vector<double> scores;
        m_answerDatabase.LoadStudents(students, scores);
        // we should be here after logging in, so m_loginStudent must have been set.
        if (students.empty() || students.front().studentNumber != m_loginStudent.studentNumber)
        {
            m_questionLibrary.Finalize();
            m_answerDatabase.Finalize();
            try{
                answers.remove();
            }catch(...){};
            try{
                questions.remove();
            }catch(...){};
            return m_questionLibrary.Initialize(m_questionLibraryFile, true)
                && m_answerDatabase.CreateDatabase(m_answerDatabaseFile, emptyInformation);
        }

        // all checks passed
        m_synQuestionCriterion.set();
        m_flagQuestionsReceived = true;
        return true;
    }

    void CClientSystem::ReceiveQuestion(CQuestionPackage &question)
    {
        if (!question.m_question)
        {
            // finished receiving questions
            // all questions are received, all answers are created,
            // now we can save right exam information.
            if (m_answerDatabase.IsInitialized())
                m_answerDatabase.SaveExamInformation(m_examInformation);
            m_flagQuestionsReceived = true;
            m_synQuestionCriterion.set();
            return;
        }

        if (!m_questionLibrary.IsInitialized())
            return;

        SharedPtr<CCriterion> c = CriterionCreator(question.m_question->Type());
        SharedPtr<CAnswer> a =AnswerCreator(question.m_question->Type());
        if (!c || !a)
            return;

        c->questionID = question.m_question->questionID;
        a->questionID = question.m_question->questionID;
        m_questionLibrary.SaveQuestion(question.m_question, c);
        m_answerDatabase.SaveAnswer(m_loginStudent, a);
    }

    void CClientSystem::ReceiveCriterion(CCriterionPackage &criterion)
    {
        if (!criterion.m_criterion)
        {
            // finished receiving questions
            m_synQuestionCriterion.set();
            return;
        }


        if (!m_questionLibrary.IsInitialized())
            return;

        SharedPtr<CQuestion> q = QuestionCreator(criterion.m_criterion->Type());
        if (!q)
            return;

        q->questionID = criterion.m_criterion->questionID;
        if (!m_questionLibrary.LoadQuestion(q))
            return;

        m_questionLibrary.SaveQuestion(q, criterion.m_criterion);
    }

    void CClientSystem::RequireQuestions(void)
    {
        // wait last operation finished
        m_synQuestionCriterion.reset();

        CPackagePtr pkg = packageManager.CreatePackage(CQuestionQueryPackage::packageType);
        pkg->WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());
    }

    void CClientSystem::RequireCriterion(void)
    {     
        if (!m_questionLibrary.IsInitialized())
            return;

        m_synQuestionCriterion.reset();

        CSafeScopedLock scope(m_questionLibraryLock);
        CPackagePtr pkg = packageManager.CreatePackage(CCriterionQueryPackage::packageType);
        CCriterionQueryPackage &query = StaticCast<CCriterionQueryPackage>(pkg);
        for (int i = 0; i < QuestionTypeCount; i++)
            m_questionLibrary.LoadAllQuestion(static_cast<QuestionType>(i), query.m_questionIds[i]);

        pkg->WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());
    }

    void CClientSystem::WaitForQuestionCriterion(void)
    {
        m_synQuestionCriterion.wait();
    }

    CQuestionLibrary & CClientSystem::GetQuestionLibrary(void)
    {
        return m_questionLibrary;
    }

    CAnswerDatabase & CClientSystem::GetAnswerDatabase(void)
    {
        return m_answerDatabase;
    }

    void CClientSystem::SendAnswerDatabase(void)
    {
        CSafeScopedLock scope(m_answerDatabaseLock);
        m_synSendingAnswerDatabase.reset();
        CPackagePtr pkg = packageManager.CreatePackage(CDatabasePackage::packageType);
        CDatabasePackage &database = StaticCast<CDatabasePackage>(pkg);
        database.SetDatabaseFile(m_answerDatabaseFile);
        pkg->WriteToStream(m_serverConnection->GetXMLConnection()->GetSocketOutputStream());
    }

    void CClientSystem::WaitForAnswerDatabaseSent(void)
    {
        m_synSendingAnswerDatabase.wait();
    }

    void CClientSystem::AnswerDatabaseSent(void)
    {
        m_flagAnswerDatabaseSent = true;
        m_synSendingAnswerDatabase.set();
    }

    bool CClientSystem::IsInitialized(void) const
    {
        return m_flagInitialized;
    }

    const StudentInfo & CClientSystem::GetLoggedInStudent(void) const
    {
        return m_loginStudent;
    }

    unsigned int CClientSystem::GetExamRestTime(void)
    {
        switch (m_examInformation.examStatus)
        {
        case ExamInformation::Preparing:
            m_examInformation.examRestTime = m_examInformation.examTotalTime;
            break;
        case ExamInformation::Processing:
            m_examInformation.examRestTime = m_examInformation.examEndTime - (unsigned int)time(NULL);
            break;
        case ExamInformation::Finished:
            m_examInformation.examRestTime = 0;
            break;
        }

        return m_examInformation.examRestTime > m_examInformation.examTotalTime ? 
            0 : m_examInformation.examRestTime;
    }

    void CClientSystem::ScoreAnswers(const ScoreParameters &param)
    {
        if (!m_answerDatabase.IsInitialized() || !m_questionLibrary.IsInitialized())
            return;

        std::vector<int> questionIds;
        for (int type = 0; type < QuestionTypeCount; type++)
        {
            SharedPtr<CAnswer> answer = AnswerCreator(static_cast<QuestionType>(type));
            SharedPtr<CCriterion> criterion = CriterionCreator(static_cast<QuestionType>(type));
            m_answerDatabase.LoadQuestions(m_loginStudent, static_cast<QuestionType>(type), questionIds);
            for (size_t i = 0; i < questionIds.size(); i++)
            {
                answer->questionID = questionIds[i];
                criterion->questionID = questionIds[i];

                m_questionLibrary.LoadCriterion(criterion);
                m_answerDatabase.LoadAnswer(m_loginStudent, answer);

                switch (type)
                {
                case typeOffice:
                    DynamicCast<CPropertyAnswer>(answer).answerParser = param.officeParser;
                    break;
                case typeHtml:
                    DynamicCast<CPropertyAnswer>(answer).answerParser = param.htmlParser;
                    break;
                case typeCoding:
                    DynamicCast<CCodingAnswer>(answer).answerChecker = param.codeChecker;
                    break;
                }

                answer->Score(criterion);
                answer->answerScore *= m_examInformation.examQuestionCoefs[type];

                m_answerDatabase.SaveAnswer(m_loginStudent, answer);
            }
        }
    }

    void CClientSystem::SetExamTempFolder(const std::string &folder)
    {
        m_examTempFolder = folder;
    }

    const std::string & CClientSystem::GetExamTempFolder(void) const
    {
        return m_examTempFolder;
    }

}
}